/*
 * Copyright (C) 2011-2025 4th Line GmbH, Switzerland and others
 *
 * The contents of this file are subject to the terms of the
 * Common Development and Distribution License Version 1 or later
 * ("CDDL") (collectively, the "License"). You may not use this file
 * except in compliance with the License. See LICENSE.txt for more
 * information.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
 *
 * SPDX-License-Identifier: CDDL-1.0
 */
package org.jupnp.support.avtransport;

import java.net.URI;

import org.jupnp.binding.annotations.UpnpAction;
import org.jupnp.binding.annotations.UpnpInputArgument;
import org.jupnp.binding.annotations.UpnpOutputArgument;
import org.jupnp.binding.annotations.UpnpService;
import org.jupnp.binding.annotations.UpnpServiceId;
import org.jupnp.binding.annotations.UpnpServiceType;
import org.jupnp.binding.annotations.UpnpStateVariable;
import org.jupnp.binding.annotations.UpnpStateVariables;
import org.jupnp.internal.compat.java.beans.PropertyChangeSupport;
import org.jupnp.model.ModelUtil;
import org.jupnp.model.types.UnsignedIntegerFourBytes;
import org.jupnp.support.avtransport.lastchange.AVTransportLastChangeParser;
import org.jupnp.support.avtransport.lastchange.AVTransportVariable;
import org.jupnp.support.lastchange.LastChange;
import org.jupnp.support.lastchange.LastChangeDelegator;
import org.jupnp.support.model.DeviceCapabilities;
import org.jupnp.support.model.MediaInfo;
import org.jupnp.support.model.PlayMode;
import org.jupnp.support.model.PositionInfo;
import org.jupnp.support.model.RecordMediumWriteStatus;
import org.jupnp.support.model.RecordQualityMode;
import org.jupnp.support.model.SeekMode;
import org.jupnp.support.model.StorageMedium;
import org.jupnp.support.model.TransportAction;
import org.jupnp.support.model.TransportInfo;
import org.jupnp.support.model.TransportSettings;
import org.jupnp.support.model.TransportState;
import org.jupnp.support.model.TransportStatus;

/**
 * Skeleton of service with "LastChange" eventing support.
 *
 * @author Christian Bauer
 * @author Amit Kumar Mondal - Code Refactoring
 */
@UpnpService(serviceId = @UpnpServiceId("AVTransport"), serviceType = @UpnpServiceType(value = "AVTransport", version = 1), stringConvertibleTypes = LastChange.class)
@UpnpStateVariables({
        @UpnpStateVariable(name = "TransportState", sendEvents = false, allowedValuesEnum = TransportState.class),
        @UpnpStateVariable(name = "TransportStatus", sendEvents = false, allowedValuesEnum = TransportStatus.class),
        @UpnpStateVariable(name = "PlaybackStorageMedium", sendEvents = false, defaultValue = "NONE", allowedValuesEnum = StorageMedium.class),
        @UpnpStateVariable(name = "RecordStorageMedium", sendEvents = false, defaultValue = "NOT_IMPLEMENTED", allowedValuesEnum = StorageMedium.class),
        @UpnpStateVariable(name = "PossiblePlaybackStorageMedia", sendEvents = false, datatype = "string", defaultValue = "NETWORK"),
        @UpnpStateVariable(name = "PossibleRecordStorageMedia", sendEvents = false, datatype = "string", defaultValue = "NOT_IMPLEMENTED"),
        @UpnpStateVariable( // TODO
                name = "CurrentPlayMode", sendEvents = false, defaultValue = "NORMAL", allowedValuesEnum = PlayMode.class),
        @UpnpStateVariable( // TODO
                name = "TransportPlaySpeed", sendEvents = false, datatype = "string", defaultValue = "1"), // 1, 1/2, 2,
                                                                                                           // -1, 1/10,
                                                                                                           // etc.
        @UpnpStateVariable(name = "RecordMediumWriteStatus", sendEvents = false, defaultValue = "NOT_IMPLEMENTED", allowedValuesEnum = RecordMediumWriteStatus.class),
        @UpnpStateVariable(name = "CurrentRecordQualityMode", sendEvents = false, defaultValue = "NOT_IMPLEMENTED", allowedValuesEnum = RecordQualityMode.class),
        @UpnpStateVariable(name = "PossibleRecordQualityModes", sendEvents = false, datatype = "string", defaultValue = "NOT_IMPLEMENTED"),
        @UpnpStateVariable(name = "NumberOfTracks", sendEvents = false, datatype = "ui4", defaultValue = "0"),
        @UpnpStateVariable(name = "CurrentTrack", sendEvents = false, datatype = "ui4", defaultValue = "0"),
        @UpnpStateVariable(name = "CurrentTrackDuration", sendEvents = false, datatype = "string"), // H+:MM:SS[.F+] or
                                                                                                    // H+:MM:SS[.F0/F1]
        @UpnpStateVariable(name = "CurrentMediaDuration", sendEvents = false, datatype = "string", defaultValue = "00:00:00"),
        @UpnpStateVariable(name = "CurrentTrackMetaData", sendEvents = false, datatype = "string", defaultValue = "NOT_IMPLEMENTED"),
        @UpnpStateVariable(name = "CurrentTrackURI", sendEvents = false, datatype = "string"),
        @UpnpStateVariable(name = "AVTransportURI", sendEvents = false, datatype = "string"),
        @UpnpStateVariable(name = "AVTransportURIMetaData", sendEvents = false, datatype = "string", defaultValue = "NOT_IMPLEMENTED"),
        @UpnpStateVariable(name = "NextAVTransportURI", sendEvents = false, datatype = "string", defaultValue = "NOT_IMPLEMENTED"),
        @UpnpStateVariable(name = "NextAVTransportURIMetaData", sendEvents = false, datatype = "string", defaultValue = "NOT_IMPLEMENTED"),
        @UpnpStateVariable(name = "RelativeTimePosition", sendEvents = false, datatype = "string"), // H+:MM:SS[.F+] or
                                                                                                    // H+:MM:SS[.F0/F1]
                                                                                                    // (in track)
        @UpnpStateVariable(name = "AbsoluteTimePosition", sendEvents = false, datatype = "string"), // H+:MM:SS[.F+] or
                                                                                                    // H+:MM:SS[.F0/F1]
                                                                                                    // (in media)
        @UpnpStateVariable(name = "RelativeCounterPosition", sendEvents = false, datatype = "i4", defaultValue = "2147483647"), // Max
                                                                                                                                // value
                                                                                                                                // means
                                                                                                                                // not
                                                                                                                                // implemented
        @UpnpStateVariable(name = "AbsoluteCounterPosition", sendEvents = false, datatype = "i4", defaultValue = "2147483647"), // Max
                                                                                                                                // value
                                                                                                                                // means
                                                                                                                                // not
                                                                                                                                // implemented
        @UpnpStateVariable(name = "CurrentTransportActions", sendEvents = false, datatype = "string"), // Play, Stop,
                                                                                                       // Pause, Seek,
                                                                                                       // Next, Previous
                                                                                                       // and Record
        @UpnpStateVariable(name = "A_ARG_TYPE_SeekMode", sendEvents = false, allowedValuesEnum = SeekMode.class), // The
                                                                                                                  // 'type'
                                                                                                                  // of
                                                                                                                  // seek
                                                                                                                  // we
                                                                                                                  // can
                                                                                                                  // perform
                                                                                                                  // (or
                                                                                                                  // should
                                                                                                                  // perform)
        @UpnpStateVariable(name = "A_ARG_TYPE_SeekTarget", sendEvents = false, datatype = "string"), // The actual seek
                                                                                                     // (offset or
                                                                                                     // whatever) value
        @UpnpStateVariable(name = "A_ARG_TYPE_InstanceID", sendEvents = false, datatype = "ui4") })
public abstract class AbstractAVTransportService implements LastChangeDelegator {

    @UpnpStateVariable(eventMaximumRateMilliseconds = 200)
    private final LastChange lastChange;
    protected final PropertyChangeSupport propertyChangeSupport;

    protected AbstractAVTransportService() {
        this.propertyChangeSupport = new PropertyChangeSupport(this);
        this.lastChange = new LastChange(new AVTransportLastChangeParser());
    }

    protected AbstractAVTransportService(LastChange lastChange) {
        this.propertyChangeSupport = new PropertyChangeSupport(this);
        this.lastChange = lastChange;
    }

    protected AbstractAVTransportService(PropertyChangeSupport propertyChangeSupport) {
        this.propertyChangeSupport = propertyChangeSupport;
        this.lastChange = new LastChange(new AVTransportLastChangeParser());
    }

    protected AbstractAVTransportService(PropertyChangeSupport propertyChangeSupport, LastChange lastChange) {
        this.propertyChangeSupport = propertyChangeSupport;
        this.lastChange = lastChange;
    }

    @Override
    public LastChange getLastChange() {
        return lastChange;
    }

    @Override
    public void appendCurrentState(LastChange lc, UnsignedIntegerFourBytes instanceId) throws Exception {

        MediaInfo mediaInfo = getMediaInfo(instanceId);
        TransportInfo transportInfo = getTransportInfo(instanceId);
        TransportSettings transportSettings = getTransportSettings(instanceId);
        PositionInfo positionInfo = getPositionInfo(instanceId);
        DeviceCapabilities deviceCaps = getDeviceCapabilities(instanceId);

        lc.setEventedValue(instanceId, new AVTransportVariable.AVTransportURI(URI.create(mediaInfo.getCurrentURI())),
                new AVTransportVariable.AVTransportURIMetaData(mediaInfo.getCurrentURIMetaData()),
                new AVTransportVariable.CurrentMediaDuration(mediaInfo.getMediaDuration()),
                new AVTransportVariable.CurrentPlayMode(transportSettings.getPlayMode()),
                new AVTransportVariable.CurrentRecordQualityMode(transportSettings.getRecQualityMode()),
                new AVTransportVariable.CurrentTrack(positionInfo.getTrack()),
                new AVTransportVariable.CurrentTrackDuration(positionInfo.getTrackDuration()),
                new AVTransportVariable.CurrentTrackMetaData(positionInfo.getTrackMetaData()),
                new AVTransportVariable.CurrentTrackURI(URI.create(positionInfo.getTrackURI())),
                new AVTransportVariable.CurrentTransportActions(getCurrentTransportActions(instanceId)),
                new AVTransportVariable.NextAVTransportURI(URI.create(mediaInfo.getNextURI())),
                new AVTransportVariable.NextAVTransportURIMetaData(mediaInfo.getNextURIMetaData()),
                new AVTransportVariable.NumberOfTracks(mediaInfo.getNumberOfTracks()),
                new AVTransportVariable.PossiblePlaybackStorageMedia(deviceCaps.getPlayMedia()),
                new AVTransportVariable.PossibleRecordQualityModes(deviceCaps.getRecQualityModes()),
                new AVTransportVariable.PossibleRecordStorageMedia(deviceCaps.getRecMedia()),
                new AVTransportVariable.RecordMediumWriteStatus(mediaInfo.getWriteStatus()),
                new AVTransportVariable.RecordStorageMedium(mediaInfo.getRecordMedium()),
                new AVTransportVariable.TransportPlaySpeed(transportInfo.getCurrentSpeed()),
                new AVTransportVariable.TransportState(transportInfo.getCurrentTransportState()),
                new AVTransportVariable.TransportStatus(transportInfo.getCurrentTransportStatus()));
    }

    public PropertyChangeSupport getPropertyChangeSupport() {
        return propertyChangeSupport;
    }

    public static UnsignedIntegerFourBytes getDefaultInstanceID() {
        return new UnsignedIntegerFourBytes(0);
    }

    @UpnpAction
    public abstract void setAVTransportURI(@UpnpInputArgument(name = "InstanceID") UnsignedIntegerFourBytes instanceId,
            @UpnpInputArgument(name = "CurrentURI", stateVariable = "AVTransportURI") String currentURI,
            @UpnpInputArgument(name = "CurrentURIMetaData", stateVariable = "AVTransportURIMetaData") String currentURIMetaData)
            throws AVTransportException;

    @UpnpAction
    public abstract void setNextAVTransportURI(
            @UpnpInputArgument(name = "InstanceID") UnsignedIntegerFourBytes instanceId,
            @UpnpInputArgument(name = "NextURI", stateVariable = "AVTransportURI") String nextURI,
            @UpnpInputArgument(name = "NextURIMetaData", stateVariable = "AVTransportURIMetaData") String nextURIMetaData)
            throws AVTransportException;

    @UpnpAction(out = {
            @UpnpOutputArgument(name = "NrTracks", stateVariable = "NumberOfTracks", getterName = "getNumberOfTracks"),
            @UpnpOutputArgument(name = "MediaDuration", stateVariable = "CurrentMediaDuration", getterName = "getMediaDuration"),
            @UpnpOutputArgument(name = "CurrentURI", stateVariable = "AVTransportURI", getterName = "getCurrentURI"),
            @UpnpOutputArgument(name = "CurrentURIMetaData", stateVariable = "AVTransportURIMetaData", getterName = "getCurrentURIMetaData"),
            @UpnpOutputArgument(name = "NextURI", stateVariable = "NextAVTransportURI", getterName = "getNextURI"),
            @UpnpOutputArgument(name = "NextURIMetaData", stateVariable = "NextAVTransportURIMetaData", getterName = "getNextURIMetaData"),
            @UpnpOutputArgument(name = "PlayMedium", stateVariable = "PlaybackStorageMedium", getterName = "getPlayMedium"),
            @UpnpOutputArgument(name = "RecordMedium", stateVariable = "RecordStorageMedium", getterName = "getRecordMedium"),
            @UpnpOutputArgument(name = "WriteStatus", stateVariable = "RecordMediumWriteStatus", getterName = "getWriteStatus") })
    public abstract MediaInfo getMediaInfo(@UpnpInputArgument(name = "InstanceID") UnsignedIntegerFourBytes instanceId)
            throws AVTransportException;

    @UpnpAction(out = {
            @UpnpOutputArgument(name = "CurrentTransportState", stateVariable = "TransportState", getterName = "getCurrentTransportState"),
            @UpnpOutputArgument(name = "CurrentTransportStatus", stateVariable = "TransportStatus", getterName = "getCurrentTransportStatus"),
            @UpnpOutputArgument(name = "CurrentSpeed", stateVariable = "TransportPlaySpeed", getterName = "getCurrentSpeed") })
    public abstract TransportInfo getTransportInfo(
            @UpnpInputArgument(name = "InstanceID") UnsignedIntegerFourBytes instanceId) throws AVTransportException;

    @UpnpAction(out = { @UpnpOutputArgument(name = "Track", stateVariable = "CurrentTrack", getterName = "getTrack"),
            @UpnpOutputArgument(name = "TrackDuration", stateVariable = "CurrentTrackDuration", getterName = "getTrackDuration"),
            @UpnpOutputArgument(name = "TrackMetaData", stateVariable = "CurrentTrackMetaData", getterName = "getTrackMetaData"),
            @UpnpOutputArgument(name = "TrackURI", stateVariable = "CurrentTrackURI", getterName = "getTrackURI"),
            @UpnpOutputArgument(name = "RelTime", stateVariable = "RelativeTimePosition", getterName = "getRelTime"),
            @UpnpOutputArgument(name = "AbsTime", stateVariable = "AbsoluteTimePosition", getterName = "getAbsTime"),
            @UpnpOutputArgument(name = "RelCount", stateVariable = "RelativeCounterPosition", getterName = "getRelCount"),
            @UpnpOutputArgument(name = "AbsCount", stateVariable = "AbsoluteCounterPosition", getterName = "getAbsCount") })
    public abstract PositionInfo getPositionInfo(
            @UpnpInputArgument(name = "InstanceID") UnsignedIntegerFourBytes instanceId) throws AVTransportException;

    @UpnpAction(out = {
            @UpnpOutputArgument(name = "PlayMedia", stateVariable = "PossiblePlaybackStorageMedia", getterName = "getPlayMediaString"),
            @UpnpOutputArgument(name = "RecMedia", stateVariable = "PossibleRecordStorageMedia", getterName = "getRecMediaString"),
            @UpnpOutputArgument(name = "RecQualityModes", stateVariable = "PossibleRecordQualityModes", getterName = "getRecQualityModesString") })
    public abstract DeviceCapabilities getDeviceCapabilities(
            @UpnpInputArgument(name = "InstanceID") UnsignedIntegerFourBytes instanceId) throws AVTransportException;

    @UpnpAction(out = {
            @UpnpOutputArgument(name = "PlayMode", stateVariable = "CurrentPlayMode", getterName = "getPlayMode"),
            @UpnpOutputArgument(name = "RecQualityMode", stateVariable = "CurrentRecordQualityMode", getterName = "getRecQualityMode") })
    public abstract TransportSettings getTransportSettings(
            @UpnpInputArgument(name = "InstanceID") UnsignedIntegerFourBytes instanceId) throws AVTransportException;

    @UpnpAction
    public abstract void stop(@UpnpInputArgument(name = "InstanceID") UnsignedIntegerFourBytes instanceId)
            throws AVTransportException;

    @UpnpAction
    public abstract void play(@UpnpInputArgument(name = "InstanceID") UnsignedIntegerFourBytes instanceId,
            @UpnpInputArgument(name = "Speed", stateVariable = "TransportPlaySpeed") String speed)
            throws AVTransportException;

    @UpnpAction
    public abstract void pause(@UpnpInputArgument(name = "InstanceID") UnsignedIntegerFourBytes instanceId)
            throws AVTransportException;

    @UpnpAction
    public abstract void record(@UpnpInputArgument(name = "InstanceID") UnsignedIntegerFourBytes instanceId)
            throws AVTransportException;

    @UpnpAction
    public abstract void seek(@UpnpInputArgument(name = "InstanceID") UnsignedIntegerFourBytes instanceId,
            @UpnpInputArgument(name = "Unit", stateVariable = "A_ARG_TYPE_SeekMode") String unit,
            @UpnpInputArgument(name = "Target", stateVariable = "A_ARG_TYPE_SeekTarget") String target)
            throws AVTransportException;

    @UpnpAction
    public abstract void next(@UpnpInputArgument(name = "InstanceID") UnsignedIntegerFourBytes instanceId)
            throws AVTransportException;

    @UpnpAction
    public abstract void previous(@UpnpInputArgument(name = "InstanceID") UnsignedIntegerFourBytes instanceId)
            throws AVTransportException;

    @UpnpAction
    public abstract void setPlayMode(@UpnpInputArgument(name = "InstanceID") UnsignedIntegerFourBytes instanceId,
            @UpnpInputArgument(name = "NewPlayMode", stateVariable = "CurrentPlayMode") String newPlayMode)
            throws AVTransportException;

    @UpnpAction
    public abstract void setRecordQualityMode(
            @UpnpInputArgument(name = "InstanceID") UnsignedIntegerFourBytes instanceId,
            @UpnpInputArgument(name = "NewRecordQualityMode", stateVariable = "CurrentRecordQualityMode") String newRecordQualityMode)
            throws AVTransportException;

    @UpnpAction(name = "GetCurrentTransportActions", out = @UpnpOutputArgument(name = "Actions", stateVariable = "CurrentTransportActions"))
    public String getCurrentTransportActionsString(
            @UpnpInputArgument(name = "InstanceID") UnsignedIntegerFourBytes instanceId) throws AVTransportException {
        try {
            return ModelUtil.toCommaSeparatedList(getCurrentTransportActions(instanceId));
        } catch (Exception e) {
            return ""; // TODO: Empty string is not defined in spec but seems reasonable for no available action?
        }
    }

    protected abstract TransportAction[] getCurrentTransportActions(UnsignedIntegerFourBytes instanceId)
            throws Exception;
}
